﻿/*
    Copyright 2012 Dmitry Bratus

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

var $wd = '/';

/*
    Changes the current working directory and returns the argument. 
    If called without the argument, returns the current working directory.
    
    path - the path to set.
*/
function cd(path) {
    if (path) {
        $wd = path;
    }

    return $wd;
}

/*
    Transforms a relative path to global.

    path - the path to transform.
*/
function globalPath(path) {
    if (!path || path.length == 0) {
        path = $wd;
    } else {
        if (path[0] != '/' && path[0] != '#') {
            path = $wd + (($wd[$wd.length - 1] != '/') ? '/' : '') + path;
        }
    }

    return path;
}

/*
    Returns a directory entry with payload by its relative path or id.

    Returns null if the entry doesn't exist.

    what - a relative path or id of the entry.
*/
function get(what) {
    return $dir.get(globalPath(what));
}

/*
    Returns a directory entry without payload by its relative path or id.

    Returns null if the entry doesn't exist.

    what - a relative path or id of the entry.
*/
function getInfo(what) {
    return $dir.getInfo(globalPath(what));
}

/*
    Returns the child entries of an entry specified by its relative path or id.

    path - the relative path or id of the parent entry.
    options - an object with the options. The following options are expected.
        offset(default 0) - how many items to skip from the begining.
        max(default 100) - the maximum number of items to return.
        entryType(default DET_ALL) - the flags indicating entries of which type to return.
        sortBy(default SORT_DEFAULT) - the sorting order.
        sort(default ASC) - the sorting direction.
        withPayload(default false) - whether to return entries with payload.
*/
function dir(path, options) {
    path = globalPath(path);

    var defaultOptions = {
        offset: 0,
        max: 100,
        entryType: DET_ALL,
        sortBy: SORT_DEFAULT,
        sort: ASC,
        withPayload: false
    };

    if (!options) {
        options = defaultOptions;
    } else {
        options = _.defaults(options, defaultOptions);
    }

    var entries = (options.withPayload) ? 
        $dir.getChildren(
            path,
            options.entryType,
            options.sortBy,
            options.sort,
            options.offset,
            options.offset + options.max
        ) :
        $dir.getChildrenInfo(
            path,
            options.entryType,
            options.sortBy,
            options.sort,
            options.offset,
            options.offset + options.max
        );

    return entries;
}

/*
    Returns the number of child enties of a specified type of a specified parent.

    path - the relative path or id of the parent entry.
    entryTypes(default DET_ALL) - the flags indicating entries of which type to return.
*/
function count(path, entryTypes) {
    path = globalPath(path);
    entryTypes = entryTypes || DET_ALL;

    return $dir.countChildren(path, entryTypes);
}

/*
    Removes an entry by its relative path or id.
    
    path - the relative path or id of the entry to remove.
*/
function rm(path) {
    path = globalPath(path);
    
    $dir.remove(path);
}

/*
    Creates a folder.

    path - the relative path of the folder to create. All non-existent folders will be created recursively.
*/
function mkdir(path) {
    path = globalPath(path);
    
    var parent = path.ancestorPath(1);
    var folderName = path.lastPathSegment();

    if (parent && folderName) {
        $dir.save(parent, new Folder({ name: folderName }));
    }
}

/*
    Renames an entry specified by an id or path.

    path - the relative path or id of the entry to rename.
    newName - the new name of the entry.
*/
function rename(path, newName) {
    path = globalPath(path);

    var ent = $dir.get(path);
    
    if (ent) {
        ent.name = newName;
        $dir.save(null, ent);
    } else {
        throw new Error("Entry '" + path + "' doesn't exist.");
    }
}

/*
    Indicated wheter an entry with a specified id or path exists.

    path - the relative path or id of the entry.
*/
function exists(path) {
    path = globalPath(path);
    return $dir.exists(path);
}

/*
    Grants permissions on an entry to a role.

    options - the operation parameters:
        to - the array of names of the roles which the permission is to be granted.
        on - the relative path or id of the entry on which the permission is to be granted.
        permission - the permission to grant.
*/
function grant(options) {
    if (!options) { throw new Error('Options must be specified.'); }
    if (!options.to) { throw new Error("Parameter 'to' must pe specified."); }
    if (!options.on) { throw new Error("Parameter 'on' must pe specified."); }
    if (!options.permission) { throw new Error("Parameter 'permission' must pe specified."); }

    var on = globalPath(options.on);
    var ent = $dir.get(on);

    if (!ent) {
        throw new Error("Entry '" + on + "' doesn't exist.");
    }

    for (var i = 0; i < options.to.length; i++) {
        if (!$dir.exists('/Roles/' + options.to[i])) {
            throw new Error("Role '" + options.to[i] + "' doesn't exist.");
        }
        
        var found = false;
        for (var j = 0; j < ent.acl.length; j++) {
            if (ent.acl[j].role.toLowerCase() == options.to[i].toLowerCase()) {
                ent.acl[j].permission = options.permission;
                found = true;
                break;
            }
        }

        if (!found) {
            ent.acl.push({ role: options.to[i], permission: options.permission });
        }
    }

    $dir.save(null, ent);
}

/*
    Revokes permissions on an entry from a role.

    options - the operation parameters:
        from - the array of names of the roles whose the permission is to be revoked.
        on - the relative path or id of the entry on which the permission is to be revoked.
        permission - the permission to revoke.
*/
function revoke(options) {
    if (!options) { throw new Error('Options must be specified.'); }
    if (!options.from) { throw new Error("Parameter 'from' must pe specified."); }
    if (!options.on) { throw new Error("Parameter 'on' must pe specified."); }
    if (!options.permission) { throw new Error("Parameter 'permission' must pe specified."); }

    var on = globalPath(options.on);
    var ent = $dir.get(on);

    if (!ent) {
        throw new Error("Entry '" + on + "' doesn't exist.");
    }

    for (var i = 0; i < options.from.length; i++) {
        for (var j = 0; j < ent.acl.length; j++) {
            if (ent.acl[j].role.toLowerCase() == options.from[i].toLowerCase()) {
                ent.acl[j].permission = ~options.permission & ent.acl[j].permission;

                if (!ent.acl[j].permission) {
                    ent.acl = _.without(ent.acl, ent.acl[j]);
                }

                break;
            }
        }
    }
    
    $dir.save(null, ent);
}

/*
    Creates a link.

    link - the relative path of the link to create.
    target - the relative path or id of the entry for the link to point.
*/
function ln(link, target) {
    link = globalPath(link);
    target = globalPath(target);

    var linkParent = link.ancestorPath(1);
    var linkName = link.lastPathSegment();

    var targetEntry = $dir.get(target);
    
    if (!targetEntry) {
        throw new Error("Target entry '" + target + "' doesn't exist.");
    }

    var linkEnt = $dir.get(link);

    if (linkEnt) {
        if (linkEnt.getType() != DET_LINK) {
            throw new Error("The entry '" + link + "' already exist and it is not a link.");
        }

        linkEnt.target = targetEntry.id;

        $dir.save(linkParent, linkEnt);
    } else {
        $dir.save(linkParent, new Link({ name: linkName, target: targetEntry.id }));
    }
}

function __makeProcessorAcls(options) {
    var acls = {
        processorAcl: [
            {
                role: options.role || 'Processor',
                permission: READ
            }
        ],
        processorFolderAcl: [
            {
                role: options.role || 'Processor',
                permission: READ | CREATE_CHILDREN
            }
        ],
        pendingFolderAcl: [
            {
                role: options.role || 'Processor',
                permission: READ | CREATE_CHILDREN
            }
        ]
    };

    var allowSchedulingTo = options.allowSchedulingTo || ['Application'];

    for (var i = 0; i < allowSchedulingTo.length; i++) {
        acls.pendingFolderAcl.push({ role: allowSchedulingTo[i], permission: READ | CREATE_CHILDREN });
        acls.processorAcl.push({ role: allowSchedulingTo[i], permission: READ });
    }

    return acls;
}

/*
    Creates or updates a processor.

    options - parameters of the operation:
        name - name of the processor.
        from - path to a processor endpoint IoC container XML configuration.
        configuration - JSON configuration of the processor.
        format(default BLOB_FORMAT_TEXT) - format of the XML configuration BLOB.
        role(default 'Processor') - name of the processor role.
        allowSchedulingTo(default ['Application']) - array of the application roles to be allowed to schedule processes for the processor.
*/
function createOrUpdateProcessor(options) {
    if (!options) { throw new Error('Options must be specified.'); }
    if (!options.name) { throw new Error("Parameter 'name' must pe specified."); }

    // Unfortunately, only BLOB_FORMAT_TEXT is safe to use, because reading XML from GZip causes
    // strange 0x1F char error.
    var config =
        (options.configuration) ?
            __jsonToSpringContainer(options.configuration, options.format || BLOB_FORMAT_TEXT) :
            ((options.from) ? $fs.loadXml(options.from, options.format || BLOB_FORMAT_TEXT) : null);

    var processorEnt = $dir.get('/Processors/' + options.name);

    if (processorEnt) {
        if (processorEnt.getType() != DET_PROCESSOR) {
            throw new Error("The entry '/Processors/" + options.name + "' already exist and it is not a processor.");
        }
        
        processorEnt.configuration = config;

        $dir.save('/Processors', processorEnt);

        changeProcessorPermissions({
            name: options.name,
            role: options.role || 'Processor',
            allowSchedulingTo: options.allowSchedulingTo || ['Application']
        });
    } else {
        var acls = __makeProcessorAcls(options);

        transaction(function () {
            var id = $dir.save('/Processors', new Processor({
                name: options.name,
                configuration: config,
                acl: acls.processorAcl
            }));

            $dir.save(id, new Folder({ name: 'Pending', acl: acls.pendingFolderAcl }));
            $dir.save(id, new Folder({ name: 'Running', acl: acls.processorFolderAcl }));
            $dir.save(id, new Folder({ name: 'Waiting', acl: acls.processorFolderAcl }));
            $dir.save(id, new Folder({ name: 'Suspended', acl: acls.processorFolderAcl }));
            $dir.save(id, new Folder({ name: 'Broken', acl: acls.processorFolderAcl }));
            $dir.save(id, new Folder({ name: 'Finished', acl: acls.processorFolderAcl }));
        });
    }
}

/*
    Activates a processor so that the processes dispatcher is able to
    start processes on it.
*/
function activateProcessor(processorName) {
    ln('/Processors/Active/' + processorName, '/Processors/' + processorName);
}

/*
    Deactivates a processor so that the processes dispatcher is not able to
    start processes on it.
*/
function deactivateProcessor(processorName) {
    rm('/Processors/Active/' + processorName);
}

/*
    Starts all migrated processes at all processors.
*/
function startMigratedProcesses() {
    var processors = $dir.getChildrenInfo('/Processors', DET_PROCESSOR, SORT_DEFAULT, ASC, 0, Number.MAX_VALUE);

    for (var i = 0; i < processors.length; i++) {
        if ($dir.exists('/Processors/Active/' + processors[i].name)) {
            var processes = $dir.getChildrenInfo('/Processors/' + processors[i].name + '/Migrated', DET_PROCESS, SORT_DEFAULT, ASC, 0, Number.MAX_VALUE);

            for (var j = 0; j < processes.length; j++) {
                $dir.move(processes[j].id, '/Processors/' + processors[i].name + '/Pending');
            }
        }
    }
}

/*
Changes permissions of an existing processor (overwrites the current permissions).

options - parameters of the operation:
name - name of the processor.
role(default 'Processor') - name of the processor role.
allowSchedulingTo(default ['Application']) - array of the application roles to be allowed to schedule processes for the processor.
*/
function changeProcessorPermissions(options) {
    if (!options.name) { throw new Error("Parameter 'name' must pe specified."); }
    if (!options.role) { throw new Error("Parameter 'role' must pe specified."); }
    if (!options.allowSchedulingTo) { throw new Error("Parameter 'allowSchedulingTo' must pe specified."); }

    var processorEnt = $dir.get('/Processors/' + options.name);

    if (processorEnt) {
        var acls = __makeProcessorAcls(options);

        transaction(function () {
            processorEnt.acl = acls.processorAcl;

            $dir.save('/Processors', processorEnt);

            var processorFolders = $dir.getChildren(processorEnt.id, DET_FOLDER, SORT_DEFAULT, ASC, 0, Number.MAX_VALUE);

            for (var i = 0; i < processorFolders.length; i++) {
                processorFolders[i].acl = 
                    (processorFolders[i].name == 'Pending') ? acls.pendingFolderAcl : acls.processorFolderAcl;

                $dir.save(null, processorFolders[i]);
            }
        });             
    }
}

/*
    Creates or updates a role.

    options - operation parameters:
        name - name of the role.
        users - array of domain user names to be added to the role.
*/
function createOrUpdateRole(options) {
    if (!options) { throw new Error('Options must be specified.'); }
    if (!options.name) { throw new Error("Parameter 'name' must pe specified."); }
    if (!options.users) { throw new Error("Parameter 'users' must pe specified."); }

    var parent = options.name.ancestorPath(1);
    var name = options.name.lastPathSegment() || options.name;

    var roleEnt = $dir.get('/Roles/' + options.name);
    
    if (roleEnt) {
        if (roleEnt.getType() != DET_ROLE) {
            throw new Error("The entry '/Roles/" + options.name + "' already exist and it is not a role.");
        }

        roleEnt.users = options.users;

        $dir.save('/Roles' + ((parent) ? '/' + parent : ''), roleEnt);
    } else {
        $dir.save('/Roles' + ((parent) ? '/' + parent : ''), new Role({
            name: name,
            users: options.users
        }));
    }
}

/*
    Adds users to a role specified by its name.

    role - name of the role.
    users - array of domain user names to add.
*/
function addUsersToRole(role, users) {
    if (!role) { throw new Error('Role must be specified.'); }
    if (!users) { throw new Error('Users must be specified.'); }

    var roleEnt = $dir.get('/Roles/' + role);
    
    if (roleEnt) {
        if (roleEnt.getType() != DET_ROLE) {
            throw new Error("Role '" + role + "' doesn't exist.");
        }

        roleEnt.users = _.union(roleEnt.users, users);

        $dir.save(('/Roles/' + role).ancestorPath(1), roleEnt);
    } else {
        throw new Error("Role '" + role + "' doesn't exist.");
    }
}

/*
    Removes users from a role specified by its name.

    role - name of the role.
    users - array of domain user names to remove.
*/
function removeUsersFromRole(role, users) {
    if (!role) { throw new Error('Role must be specified.'); }
    if (!users) { throw new Error('Users must be specified.'); }

    var roleEnt = $dir.get('/Roles/' + role);

    if (roleEnt) {
        if (roleEnt.getType() != DET_ROLE) {
            throw new Error("Role '" + role + "' doesn't exist.");
        }

        roleEnt.users = _.difference(roleEnt.users, users);

        $dir.save(('/Roles/' + role).ancestorPath(1), roleEnt);
    } else {
        throw new Error("Role '" + role + "' doesn't exist.");
    }
}

/*
    Returns an array of domain user names of a role.
*/
function usersOfRole(role) {
    if (!role) { throw new Error('Role must be specified.'); }
    
    var roleEnt = $dir.get('/Roles/' + role);

    if (roleEnt) {
        return roleEnt.users;
    } else {
        throw new Error("Role '" + role + "' doesn't exist.");
    }
}

/*
    Creates or updates a service container.

    options - operation parameters:
        name - the name of the service container (i.e. the part of its path after /Services).
        from - the name of the XML IoC container configuration file.
        configuration - JSON configuration of the processor.
        format(default BLOB_FORMAT_TEXT) - the format of the configuration BLOB.
*/
function createOrUpdateServiceContainer(options) {
    if (!options) { throw new Error('Options must be specified.'); }
    if (!options.name) { throw new Error("Parameter 'name' must pe specified."); }
    if (!options.from && !options.configuration) { throw new Error("Either 'from' or 'configuration' must pe specified."); }

    var parent = options.name.ancestorPath(1);
    var name = options.name.lastPathSegment() || options.name;
    
    // Unfortunately, only BLOB_FORMAT_TEXT is safe to use, because reading XML from GZip causes
    // strange 0x1F char error.
    var config =
        (options.configuration) ?
            __jsonToSpringContainer(options.configuration, options.format || BLOB_FORMAT_TEXT) :
            $fs.loadXml(options.from, options.format || BLOB_FORMAT_TEXT);

    var svcContEnt = $dir.get('/Services/' + options.name);

    if (svcContEnt) {
        if (svcContEnt.getType() != DET_SERVICE_CONTAINER) {
            throw new Error("The entry '/Services/" + options.name + "' already exist and it is not a service container.");
        }

        svcContEnt.configuration = config;

        $dir.save('/Services' + ((parent) ? '/' + parent : ''), svcContEnt);
    } else {
        $dir.save('/Services' + ((parent) ? '/' + parent : ''), new ServiceContainer({
            name: name,
            configuration: config
        }));
    }
}

/*
    Creates or updates a set.

    options - operation parameters:
        at - the path of the set.
        messageType - the assembly qualified name of the message type.
*/
function createOrUpdateSet(options) {
    if (!options) { throw new Error('Options must be specified.'); }
    if (!options.at) { throw new Error("Parameter 'at' must pe specified."); }
    if (!options.messageType) { throw new Error("Parameter 'messageType' must pe specified."); }

    var at = globalPath(options.at);

    var parent = at.ancestorPath(1);
    var name = at.lastPathSegment();

    if (!parent) {
        throw new Error("Invalid set location '" + options.at + "'.");
    }

    var setEnt = $dir.get(at);
    
    if (setEnt) {
        if (setEnt.getType() != DET_SET) {
            throw new Error("The entry '" + options.at + "' already exist and it is not a set.");
        }

        setEnt.messageType = options.messageType;
        setEnt.capacity = options.capacity || 0;

        $dir.save(parent, setEnt);
    } else {
        $dir.save(parent, new Set({
            name: name,
            messageType: options.messageType,
            capacity: options.capacity || 0
        }));
    }
}

/*
    Creates or updates a workflow.

    options - operation parameters:
        name - the name of the workflow (i.e. the part of the path after '/Workflows').
        definitionType - the assembly qualified name of the workflow definition type. May not be specified for update.
        preferredProcessors - the array of names of the processors dedicated to execute processes of this workflow. May not be specified.
*/
function createOrUpdateWorkflow(options) {
    if (!options) { throw new Error('Options must be specified.'); }
    if (!options.name) { throw new Error("Parameter 'name' must pe specified."); }

    var parent = options.name.ancestorPath(1);
    var name = options.name.lastPathSegment() || options.name;

    var wfEnt = $dir.get('/Workflows/' + options.name);
    
    if (wfEnt) {
        if (wfEnt.getType() != DET_WORKFLOW) {
            throw new Error("The entry '/Workflows/" + options.name + "' already exist and it is not a workflow.");
        }

        if (options.definitionType) {
            wfEnt.definitionType = options.definitionType;
        }
        
        if (options.preferredProcessors) {
            wfEnt.preferredProcessors = options.preferredProcessors;
        }

        $dir.save('/Workflows' + ((parent) ? '/' + parent : ''), wfEnt);
    } else {
        if (!options.definitionType) { throw new Error("Parameter 'definitionType' must pe specified."); }
        
        $dir.save('/Workflows' + ((parent) ? '/' + parent : ''), new Workflow({
            name: name,
            definitionType: options.definitionType,
            preferredProcessors: options.preferredProcessors || []
        }));
    }
}

/*
    Switches a process to the pending state.
    
    pid - the id of the process.
*/
function retryProcess(pid) {
    __changeProcessStatus(pid, 'Pending');
}

/*
    Switches all waiting processes to the pending state.

    processorName(optional) - if specified, only the processes of the specified processor are switched; 
                              otherwise, the operation is applied to all processors.
*/
function retryWaiting(processorName) {
    var processorNames;
    var i, j;

    if (!processorName) {
        var processors = $dir.getChildrenInfo('/Processors', DET_PROCESSOR, SORT_DEFAULT, ASC, 0, Number.MAX_VALUE);
        processorNames = [];

        for (i = 0; i < processors.length; i++) {
            processorNames.push(processors[i].name);
        }
    } else {
        processorNames = [processorName];
    }

    for (i = 0; i < processorNames.length; i++) {
        var procs =
            $dir.getChildrenInfo(
                '/Processors/' + processorNames[i] + '/Waiting',
                DET_PROCESS,
                SORT_DEFAULT,
                ASC,
                0,
                Number.MAX_VALUE
            );

        for (j = 0; j < procs.length; j++) {
            retryProcess(procs[j].id);
        }
    }
}